/*
 * Copyright 2008 Zoundry LLC, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.zoundry.gaedt.sdk;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Properties;
import java.util.regex.Matcher;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.zoundry.gaedt.core.util.ZGaeCloser;
import org.zoundry.gaedt.core.util.ZGaeFileUtil;
import org.zoundry.gaedt.core.util.zip.IZGaeZipUnpackListener;
import org.zoundry.gaedt.core.util.zip.ZGaeZipUnpacker;


/**
 * This is responsible for updating the downloaded GAE SDK to the latest
 * SDK version.  If the SDK is already up to date, this updater will do
 * nothing.  If the SDK is *not* up to date, it will either automatically
 * update the SDK (if no previous version was found) or prompt the user
 * whether they want to update the now.
 *
 * Download the latest SDK.
 *   - hit the URL: http://code.google.com/appengine/downloads.html
 *   - parse the resulting HTML or use regexp to find google_appengine_***.zip
 *   - download google_appengine_***.zip
 *   - unpack google_appengine_***.zip to a standard .metadata location
 *   - create gaesdk.properties file - store latest sdk information
 */
public class ZGaeSDKUpdater
{
    /**
     * C'tor.
     */
    public ZGaeSDKUpdater()
    {
    }

    /**
     * Update the SDK.
     *
     * @param aProgressMonitor
     */
    public void update(IProgressMonitor aProgressMonitor) throws Exception
    {
        aProgressMonitor.beginTask("Updating the Google App Engine SDK", 4);
        // Get the current SDK
        ZGaeSDK sdk = getCurrentSDK();

        // Get the latest SDK available at Google
        ZGaeLatestSDKInfo latestSDKInfo = getLatestSDKInfo(aProgressMonitor);
        if (latestSDKInfo == null)
        {
            aProgressMonitor.subTask("No SDK information found.");
            Thread.sleep(2000);
            aProgressMonitor.worked(4);
            aProgressMonitor.done();
            return;
        }
        else
        {
            aProgressMonitor.worked(1);
        }

        checkForCancel(aProgressMonitor);

        // If we are up-to-date, then there is nothing more to do.
        if (latestSDKInfo.getVersion().equals(sdk.getVersion()))
        {
            aProgressMonitor.subTask("SDK is already up to date (version " + sdk.getVersion() + ").");
            aProgressMonitor.worked(3);
            Thread.sleep(2000);
            aProgressMonitor.done();
            return;
        }

        // If we are *not* up-to-date, then download the latest SDK from Google
        String msg = MessageFormat.format("Downloading latest Google App Engine SDK [version {0}]",
                latestSDKInfo.getVersion());
        aProgressMonitor.subTask(msg);
        File downloadedSDK = downloadSDK(latestSDKInfo, aProgressMonitor);
        if (downloadedSDK == null)
        {
            aProgressMonitor.done();
            return;
        }
        else
        {
            aProgressMonitor.worked(1);
        }

        checkForCancel(aProgressMonitor);

        // Now that the SDK has been downloaded, unpack it
        MessageFormat.format("Unpacking latest Google App Engine SDK [version {0}]", latestSDKInfo.getVersion());
        aProgressMonitor.subTask(msg);
        unpackSDK(downloadedSDK, sdk, aProgressMonitor);
        aProgressMonitor.worked(1);

        // Now create the gaesdk.properties file in the sdk location.
        createSDKProperties(latestSDKInfo, sdk);
        aProgressMonitor.worked(1);

        // Now refresh the SDK (since we have a new .props file)
        sdk.refresh();

        // Now we're done.
        aProgressMonitor.done();
    }

    /**
     * Checks the progress monitor to see if the user cancelled the job.
     *
     * @param aProgressMonitor
     */
    protected void checkForCancel(IProgressMonitor aProgressMonitor)
    {
        if (aProgressMonitor.isCanceled())
            throw new OperationCanceledException();
    }

    /**
     * Returns the currently installed SDK.
     */
    protected ZGaeSDK getCurrentSDK()
    {
        return ZGaeSDK.getInstance();
    }

    /**
     * Gets information about the latest GAE SDK.
     *
     * @param aProgressMonitor
     */
    private ZGaeLatestSDKInfo getLatestSDKInfo(IProgressMonitor aProgressMonitor) throws Exception
    {
        URL downloadPageUrl = new URL(IZGaeSDKConstants.SDK_DOWNLOAD_SITE);
        InputStream iStream = null;
        BufferedReader reader = null;
        try
        {
            iStream = downloadPageUrl.openStream();
            reader = new BufferedReader(new InputStreamReader(iStream));
            String line = null;
            while ( (line = reader.readLine()) != null )
            {
                Matcher matcher = IZGaeSDKConstants.SDK_URL_PATTERN.matcher(line);
                if (matcher != null && matcher.find())
                {
                    URL downloadUrl = new URL(matcher.group(0));
                    String version = matcher.group(1);
                    return new ZGaeLatestSDKInfo(version, downloadUrl);
                }
            }
        }
        finally
        {
            ZGaeCloser.close(iStream);
            ZGaeCloser.close(reader);
        }
        return null;
    }

    /**
     * Downloads the latest SDK .zip file to the local hard drive.
     *
     * @param aLatestSDKInfo
     * @param aProgressMonitor
     */
    private File downloadSDK(ZGaeLatestSDKInfo aLatestSDKInfo, final IProgressMonitor aProgressMonitor) throws Exception
    {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        File tempFile = File.createTempFile("_zgae_", ".zip");
        tempFile.deleteOnExit();
        try
        {
            inputStream = aLatestSDKInfo.getDownloadUrl().openStream();
            outputStream = new ZGaeSDKUpdaterOutputStream(new FileOutputStream(tempFile), aProgressMonitor);
            ZGaeFileUtil.copy(inputStream, outputStream, new ZGaeFileUtil.IZGaeCancelator()
            {
                /**
                 * @see org.zoundry.gaedt.core.util.ZGaeFileUtil.IZGaeCancelator#isCanceled()
                 */
                public boolean isCanceled()
                {
                    return aProgressMonitor.isCanceled();
                }
            });
        }
        finally
        {
            ZGaeCloser.close(inputStream);
            ZGaeCloser.close(outputStream);
        }
        return tempFile;
    }

    /**
     * Unpacks the downloaded SDK .zip to the right place in the local
     * eclipse installation.
     *
     * @param aDownloadedSDK
     * @param aSdk
     * @param aProgressMonitor
     */
    private void unpackSDK(File aDownloadedSDK, ZGaeSDK aSdk, IProgressMonitor aProgressMonitor) throws Exception
    {
        String location = aSdk.getLocation();
        File sdkDir = new File(location);
        if (sdkDir.isDirectory())
            ZGaeFileUtil.recursivelyDelete(sdkDir);
        else
            ZGaeFileUtil.makeDirs(sdkDir);
        ZGaeZipUnpacker unpacker = new ZGaeZipUnpacker(new ZGaeSDKUpdaterZipListener(aProgressMonitor));
        unpacker.unpack(aDownloadedSDK, sdkDir);
    }

    /**
     * Creates the gaesdk.properties file in the SDK location.
     *
     * @param aLatestSDKInfo
     * @param aSdk
     * @throws IOException
     */
    protected void createSDKProperties(ZGaeLatestSDKInfo aLatestSDKInfo, ZGaeSDK aSdk) throws IOException
    {
        Properties props = new Properties();
        props.setProperty("version", aLatestSDKInfo.getVersion());
        props.setProperty("sdk-location", aLatestSDKInfo.getDownloadUrl().toExternalForm());
        props.setProperty("timestamp", ZGaeSDK.sDateFormat.format(new Date()));

        File outFile = new File(aSdk.getLocation(), "gaesdk.properties");
        OutputStream oStream = null;
        try
        {
            oStream = new FileOutputStream(outFile);
            props.store(oStream, "Generated GAE SDK information (from SDK updater)");
        }
        finally
        {
            ZGaeCloser.close(oStream);
        }
    }

    /**
     * Output stream wrapper for adding support for updating the
     * progress monitor during download of the sdk zip file.
     */
    protected static class ZGaeSDKUpdaterOutputStream extends OutputStream
    {
        /** The output stream to delegate calls to. */
        private final OutputStream mOutputStream;
        /** The progress monitor to update during download. */
        private final IProgressMonitor mProgressMonitor;
        /** Total bytes written. */
        private int mTotalBytes = 0;

        /**
         * C'tor.
         *
         * @param aOutputStream
         * @param aProgressMonitor
         */
        public ZGaeSDKUpdaterOutputStream(OutputStream aOutputStream,
                IProgressMonitor aProgressMonitor)
        {
            mOutputStream = aOutputStream;
            mProgressMonitor = aProgressMonitor;
        }

        /**
         * @see java.io.OutputStream#write(int)
         */
        @Override
        public void write(int aData) throws IOException
        {
            mOutputStream.write(aData);
        }

        /**
         * @see java.io.OutputStream#write(byte[], int, int)
         */
        @Override
        public void write(byte[] aB, int aOff, int aLen) throws IOException
        {
            super.write(aB, aOff, aLen);
            mTotalBytes += aLen;
            float numMBytes = mTotalBytes / 1024f / 1024f;
            String message = MessageFormat.format("Downloading Google App Engine SDK [{0,number,#.##} MB so far]", numMBytes);
            mProgressMonitor.subTask(message);
        }

        /**
         * @see java.io.OutputStream#flush()
         */
        @Override
        public void flush() throws IOException
        {
            mOutputStream.flush();
        }

        /**
         * @see java.io.OutputStream#close()
         */
        @Override
        public void close() throws IOException
        {
            mOutputStream.close();
        }
    }

    /**
     * Zip unpack listener used to update the progress monitor.
     */
    public static class ZGaeSDKUpdaterZipListener implements IZGaeZipUnpackListener
    {
        private final IProgressMonitor mProgressMonitor;

        /**
         * @param aProgressMonitor
         */
        public ZGaeSDKUpdaterZipListener(IProgressMonitor aProgressMonitor)
        {
            mProgressMonitor = aProgressMonitor;
        }

        /**
         * @see org.zoundry.gaedt.core.util.zip.IZGaeZipUnpackListener#onZipEntryUnpacked(java.lang.String)
         */
        public void onZipEntryUnpacked(String aEntryName)
        {
            String msg = MessageFormat.format("Unpacking latest Google App Engine SDK [{0}]", aEntryName);
            mProgressMonitor.subTask(msg);
        }

        /**
         * @see org.zoundry.gaedt.core.util.zip.IZGaeZipUnpackListener#onZipUnpackCompleted()
         */
        public void onZipUnpackCompleted()
        {
            mProgressMonitor.subTask("Finished unpacking latest Google App Engine SDK");
        }

        /**
         * @see org.zoundry.gaedt.core.util.zip.IZGaeZipUnpackListener#onZipUnpackStarted(int)
         */
        public void onZipUnpackStarted(int aNumEntries)
        {
            mProgressMonitor.subTask("Unpacking latest Google App Engine SDK");
        }
    }
}
